from __future__ import print_function
import argparse
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms

import collections


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1, padding=2, bias=True)
        self.conv3 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0, bias=True)
        self.conv5 = nn.Conv2d(in_channels=16, out_channels=120, kernel_size=5, stride=1, padding=0, bias=True)
        self.conv3_drop = nn.Dropout2d()  # 随机选择输入的信道，将其设为0
        self.fc6 = nn.Linear(in_features=120, out_features=84, bias=True)
        self.fc7 = nn.Linear(in_features=84, out_features=10, bias=False)

    def forward(self, input):
        o_c1s2 = F.relu(F.max_pool2d(self.conv1(input), 2))

        o_c3s4 = F.relu(F.max_pool2d(self.conv3_drop(self.conv3(o_c1s2)), 2))
        o_c5 = F.relu(self.conv5(o_c3s4)).view(-1, 120)

        f6 = F.relu(self.fc6(o_c5))
        f6 = F.dropout(f6, training=self.training)
        output = self.fc7(f6)
        if False:
            print('\nForward debug')
            print('input:', input.shape)
            print('out C1 S2:', o_c1s2.shape)
            print('out C3 S4:', o_c3s4.shape)
            print('out C5->C5:', o_c5.shape)
            print('out F6:', f6.shape)
            print('output:', output.shape)

        return F.log_softmax(output, dim=1)


def train(log_interval, model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                       100. * batch_idx / len(train_loader), loss.item()))


def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item()  # sum up batch loss
            pred = output.max(1, keepdim=True)[1]  # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))


def main():
    config = {
        'device': 'cuda',  # cuda / cpu
        'seed': 1,
        'epochs': 100,
        'log_interval': 10,
        'batch_size': 64,
        'test_batch_size': 1000,
        'lr': 0.01,
        'momentum': 0.5
    }
    use_cuda = True

    torch.manual_seed(config['seed'])

    device = torch.device(config['device'])

    kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
    train_loader = torch.utils.data.DataLoader(datasets.MNIST('../data', train=True, download=True,
                                                              transform=transforms.Compose([transforms.ToTensor(),
                                                                                            transforms.Normalize(
                                                                                                (0.1307,),
                                                                                                (0.3081,))])),
                                               batch_size=config['batch_size'], shuffle=True, **kwargs)
    test_loader = torch.utils.data.DataLoader(datasets.MNIST('../data', train=False, transform=transforms.Compose(
        [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])), batch_size=config['test_batch_size'],
                                              shuffle=True, **kwargs)

    model = Net().to(device)
    optimizer = optim.SGD(model.parameters(), lr=config['lr'], momentum=config['momentum'])

    for epoch in range(1, config['epochs'] + 1):
        train(config['log_interval'], model, device, train_loader, optimizer, epoch)
        test(model, device, test_loader)
    torch.save(model.state_dict(), 'mnist0.lenet5.torch.weights')


def read_weights():
    device = torch.device('cuda')
    weights = torch.load('mnist0.lenet5.torch.weights')

    keys = tuple(weights)
    for key in keys:
        print(key, weights[key].shape)
    kwargs = {'num_workers': 1, 'pin_memory': True}
    test_loader = torch.utils.data.DataLoader(datasets.MNIST('../data', train=False, transform=transforms.Compose(
        [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])), batch_size=1000,
                                              shuffle=True, **kwargs)

    model = Net().to(device)
    model.load_state_dict(weights)

    test(model, device, test_loader)


def fine_train():
    use_cuda = True
    config = {
        'device': 'cuda',  # cuda / cpu
        'seed': 1,
        'epochs': 10,
        'log_interval': 10,
        'batch_size': 64,
        'test_batch_size': 1000,
        'lr': 0.01,
        'momentum': 0.5
    }
    device = torch.device('cuda')
    weights = torch.load('mnist0.torch.weights')

    kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
    train_loader = torch.utils.data.DataLoader(datasets.MNIST('../data', train=True, download=True,
                                                              transform=transforms.Compose([transforms.ToTensor(),
                                                                                            transforms.Normalize(
                                                                                                (0.1307,),
                                                                                                (0.3081,))])),
                                               batch_size=config['batch_size'], shuffle=True, **kwargs)
    test_loader = torch.utils.data.DataLoader(datasets.MNIST('../data', train=False, transform=transforms.Compose(
        [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])), batch_size=config['test_batch_size'],
                                              shuffle=True, **kwargs)

    model = Net().to(device)
    optimizer = optim.SGD(model.parameters(), lr=config['lr'], momentum=config['momentum'])
    model.load_state_dict(weights)

    for epoch in range(1, config['epochs'] + 1):
        train(config['log_interval'], model, device, train_loader, optimizer, epoch)
        test(model, device, test_loader)
    torch.save(model.state_dict(), 'mnist0.torch.weights')


if __name__ == '__main__':
    read_weights()
